package com.wechat.websocket;

import com.alibaba.fastjson.JSON;
import com.wechat.entity.constants.Constants;
import com.wechat.entity.dto.MessageSendDto;
import com.wechat.entity.dto.WsInitData;
import com.wechat.entity.enums.*;
import com.wechat.entity.po.*;
import com.wechat.entity.query.*;
import com.wechat.mappers.*;
import com.wechat.redis.RedisComponent;
import com.wechat.service.ChatSessionUserService;
import com.wechat.utils.JsonUtils;
import com.wechat.utils.StringTools;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
/*
 简介：
 USER_CONTEXT_MAP 映射
 1.用户连接管理：通过 userId 快速查找对应的 Channel 连接
 2.消息定向发送：根据用户 ID 找到对应的 Channel，实现点对点消息传输
 3.连接状态维护：管理用户的在线/离线状态
 GROUP_CONTEXT_MAP 映射
 1.群组连接管理：通过 groupId 管理群组内的所有用户连接
 2.群消息广播：根据群组 ID 找到所有成员的 Channel，实现群发消息
 3.群组成员维护：动态管理群组成员的加入和离开
 */
@Component("channelContextUtils")
public class ChannelContextUtils {

    private static final Logger logger = LoggerFactory.getLogger(ChannelContextUtils.class);
    //private static final AttributeKey<String> USER_ID = AttributeKey.newInstance("userId");

    @Resource
    private RedisComponent redisComponent;

    //用户id与channel的映射
    public static final ConcurrentMap<String, Channel> USER_CONTEXT_MAP = new ConcurrentHashMap();

    //群组id与channel的映射
    public static final ConcurrentMap<String, ChannelGroup> GROUP_CONTEXT_MAP = new ConcurrentHashMap();

    @Resource
    @Lazy//懒加载,避免循环依赖
    private ChatSessionUserService chatSessionUserService;

    @Resource
    private UserInfoMapper<UserInfo, UserInfoQuery> userInfoMapper;

    @Resource
    private UserContactMapper<UserContact, UserContactQuery> userContactMapper;

    @Resource
    private UserContactApplyMapper<UserContactApply, UserContactApplyQuery> userContactApplyMapper;

    @Resource
    private ChatSessionUserMapper<ChatSessionUser, ChatSessionUserQuery> chatSessionUserMapper;

    @Resource
    private ChatMessageMapper<ChatMessage, ChatMessageQuery> chatMessageMapper;


    /**
     * 创建会话连接，将用户的channel与用户id关联起来,使用AttributeKey
     * 每个Channel有唯一的channelId，通过channelId创建唯一的AttributeKey，再将userId绑定到AttributeKey中
     *键值对，键为channelId，值为用户id
     * 每个websocket连接对应一个channel，一个channel对应一个用户的单次连接，但是一个用户可以有多个channel
     * @param userId
     * @param channel
     */
    public void addContext(String userId, Channel channel) {
        String channelId = channel.id().toString();//获取当前用户channel的channelId
        //为什么可以判断该channel是否已经绑定过用户id：因为Netty生成的channelId全局唯一，当用户断开连接重连会创建一个新的channelId
        AttributeKey<String> attributeKey = AttributeKey.newInstance(channelId);//判断该channel是否已经绑定过用户id，创建/返回一个AttributeKey
        logger.info("用户{}加入连接，channelId为{}", userId, channelId);
        channel.attr(attributeKey).set(userId);//一个channel绑定一个用户id，一个id可以绑定多个channel

        //判断联系对象是否是群组
        List<String> userContactList = redisComponent.getUserContactList(userId);
        for (String contactId : userContactList) {
            if(contactId.startsWith(UserContactTypeEnum.GROUP.getPrefix())){
                add2Group(contactId, channel);//如果是联系对象是群组
            }
        }
        //联系对象是用户
        USER_CONTEXT_MAP.put(userId, channel);//加入用户map管理
        redisComponent.saveUserHeartBeat(userId);//一旦连接上就更新用户心跳

        //更新用户最后连接时间
        UserInfo updateInfo = new UserInfo();
        updateInfo.setLastLoginTime(new Date());
        userInfoMapper.updateByUserId(updateInfo,userId);

        //给用户发送信息推送（显示3天内离线的信息）
        UserInfo userInfo = userInfoMapper.selectByUserId(userId);//查询用户信息
        Long dbLastOffTime = userInfo.getLastOffTime();//获取用户最后退出会话的时间
        Long queryStartTime = null;
       //如果用户最后退出会话时间超过3天，则从3天前开始查询（即支持查询历史3天聊天记录）
        //获取查询的起始时间
        if(dbLastOffTime != null){
            //计算从哪个时间点开始查询信息
            Long threeDaysAgo = System.currentTimeMillis() - Constants.MILLISECOND_3DAYS_AGO;
            //取用户最后退出会话时间与三天前的时间中的较大值作为起始查询时间点
            queryStartTime = Math.max(threeDaysAgo, dbLastOffTime);
        }else {
            //没有最后退出会话时间，则从三天前开始查询
            queryStartTime = System.currentTimeMillis() - Constants.MILLISECOND_3DAYS_AGO;
        }

        //1.支持查询历史全部会话信列表 查寻用户所有的会话信息，保证换设备登录时，用户会话信息不变
        ChatSessionUserQuery query = new ChatSessionUserQuery();
        query.setUserId(userId);
        query.setOrderBy("last_receive_time desc");
        //List<ChatSessionUser> chatSessionUserList=chatSessionUserService.findListByParam(query);
        List<ChatSessionUser> chatSessionUserList = chatSessionUserMapper.selectList(query);//查询用户所有的会话信息

        WsInitData wsInitData = new WsInitData();//创建初始化数据
        wsInitData.setChatSessionList(chatSessionUserList);//添加会话列表

        //2.查询自己的（离线）聊天记录（也就是说，我自己是他人的联系人，我充当联系人的位置，且还有自己的群组）
        //查所有的联系人：群组+自己（以他人视角）
        //filter是过滤集合中的元素，这里是保留群组联系id
        List<String> groupList = userContactList.stream().filter(item -> item.startsWith(UserContactTypeEnum.GROUP.getPrefix())).collect(Collectors.toList());
        groupList.add(userId);
        ChatMessageQuery chatMessageQuery = new ChatMessageQuery();
        chatMessageQuery.setContactIdList(groupList);//添加查询条件：使用 联系人 列表来查
        chatMessageQuery.setLastReceiveTime(queryStartTime);//添加查询条件：最后接收时间（3天内）
        List<ChatMessage> chatMessageList = chatMessageMapper.selectList(chatMessageQuery);

        wsInitData.setChatMessageList(chatMessageList);//添加消息列表

        //3.提示好友申请(离线状态3天内错过的信息)，这里是用户在离线后添加的申请的提示信息，具体申请在列表内还是存在的
        UserContactApplyQuery applyQuery = new UserContactApplyQuery();
        applyQuery.setReceiveUserId(userId);
        applyQuery.setLastApplyTimestamp(queryStartTime);
        applyQuery.setStatus(UserContactApplyStatusEnum.INIT.getStatus());
        Integer applyCount = userContactApplyMapper.selectCount(applyQuery);

        wsInitData.setApplyCount(applyCount);
        //发送消息
        MessageSendDto messageSendDto = new MessageSendDto();
        messageSendDto.setMessageType(MessageTypeEnum.INIT.getType());
        messageSendDto.setContactId(userId);
        messageSendDto.setExtendData(wsInitData);//扩展数据
        sendMsg(messageSendDto, userId);

    }


    /**
     * 用户被异常断开连接，管理用户channel以及心跳
     * 这个是channel断开后，通过HandlerWebSocket.removeContext()方法netty主动调用，删除心跳记录
     * @param channel
     */
    public void removeContext(Channel channel) {
        //获取用户id
        Attribute<String> attribute = channel.attr(AttributeKey.valueOf(channel.id().toString()));//从绑定的channel中获取用户id
        String userId = attribute.get();
        if(!StringTools.isEmpty(userId)){
            USER_CONTEXT_MAP.remove(userId);//从UserMap中删除用户通道
            //从群组中删除该用户
            redisComponent.getUserContactList(userId).forEach(contactId->{
                if (contactId.startsWith(UserContactTypeEnum.GROUP.getPrefix())) {
                    ChannelGroup groupContact = GROUP_CONTEXT_MAP.get(contactId);
                    if(groupContact != null) {
                        groupContact.remove(channel);
                    }
                }
            });
        }
        redisComponent.removeUserHeartBeat(userId);//删除用户心跳
        //更新用户最后离线时间（退出会话的时间）
        UserInfo updateInfo = new UserInfo();
        updateInfo.setLastOffTime(new Date().getTime());
        userInfoMapper.updateByUserId(updateInfo,userId);
    }

    /**
     * 断开通道连接---根据用户id操作
     * 这个是主动断开ws连接，清除用户token，功能：主动登出，修改密码，强制下线
     * @param userId
     */
    public void closeContext(String userId) {
        if(StringTools.isEmpty(userId)){
            return;
        }
        redisComponent.cleanUserTokenByUserId(userId);//清除当前用户的token信息
        Channel channel = USER_CONTEXT_MAP.get(userId);
        if(channel == null){
            return;
        }
        channel.close();//主动断开ws连接，之后触发channelInactive，删除用户
    }

    /**
     * 发送消息(单+群)给客户端
     * @param messageSendDto
     */
    public void sendMessage(MessageSendDto messageSendDto) {
        String contactId = messageSendDto.getContactId();
        UserContactTypeEnum contactType = UserContactTypeEnum.getByPrefix(contactId);
//        // 如果是撤回消息，特殊处理
//        if (MessageTypeEnum.WITHDRAW.getType().equals(messageSendDto.getMessageType())) {
//            handleWithdrawMessage(messageSendDto, contactType);
//            return;
//        }
        switch (contactType){
            case USER:
                sendMsg2User(messageSendDto);
                break;
            case GROUP:
                sendMsg2Group(messageSendDto);
                break;
        }

    }

    /**
     * 发送消息（单聊）
     * @param messageSendDto
     * @param receiveId 接收者id
     */
    private static void sendMsg(MessageSendDto messageSendDto, String receiveId) {
        Channel userChannel = USER_CONTEXT_MAP.get(receiveId);//获取联系人的通道
        if(userChannel == null){
            return;
        }
        if (MessageTypeEnum.ADD_FRIEND_SELF.getType().equals(messageSendDto.getMessageType())) {
            //处理添加联系人的相关信息发送
            //自己给自己发送消息（页面显示，默认打招呼）---特殊处理，不需要服务端转发，直接客户端本地渲染，所以不需要前端的增加红点提示，这里拿到UserInfo就是关键所在
            UserInfo userInfo =(UserInfo) messageSendDto.getExtendData();//服务端通过extendData直接传递用户信息，客户端不需要再向服务端请求用户信息
            messageSendDto.setMessageType(MessageTypeEnum.ADD_FRIEND.getType());
            messageSendDto.setContactId(userInfo.getUserId()); //设置联系人为被添加人
            messageSendDto.setContactName(userInfo.getNickName());
            messageSendDto.setExtendData(null);
        }else{//处理普通聊天信息
            //消息回显：让发送方看到自己发送的消息
            messageSendDto.setContactId(messageSendDto.getSendUserId());
            messageSendDto.setContactName(messageSendDto.getSendUserNickName());
        }
        //服务端发送消息给客户端
        userChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(messageSendDto)));//Java对象需要序列化为字符串后通过ws传输

    }


    /**
     * 发送消息给用户（单聊）
     * 发送普通聊天信息+添加好友消息+强制下线消息
     */
    private void sendMsg2User(MessageSendDto messageSendDto) {
        String receiveId = messageSendDto.getContactId();
        if(receiveId == null){
            return;
        }
        sendMsg(messageSendDto, receiveId);
        //强制下线
        if(MessageTypeEnum.FORCE_OFF_LINE.getType().equals(messageSendDto.getMessageType())){
            //关闭通道连接
            closeContext(receiveId);
        }

    }

    /**
     * 发送消息到组（群聊）
     * 发送群聊信息+添加群消息+群成员退出群聊消息+群成员移除群聊消息+解散群聊消息
     */
    private void sendMsg2Group(MessageSendDto messageSendDto) {
        String groupId = messageSendDto.getContactId();
        if(groupId == null){
            return;
        }
        ChannelGroup channelGroup = GROUP_CONTEXT_MAP.get(groupId);//获取群组的通道
        if(channelGroup == null){
            return;
        }
        //这里不分MessageTypeEnum，对所有消息类型进行群组广播
        channelGroup.writeAndFlush(new TextWebSocketFrame(JsonUtils.convertObj2Json(messageSendDto)));//Java对象需要序列化为字符串后通过ws传输

        //特殊处理几种消息（特殊处理的方向是channel管理而非发送信息处理）
        MessageTypeEnum messageType = MessageTypeEnum.getByType(messageSendDto.getMessageType());
        //从群组中移除成员（主动退群/被移除群聊）：使用remove()方法
        if(MessageTypeEnum.LEAVE_GROUP.equals(messageType) || MessageTypeEnum.REMOVE_GROUP.equals(messageType)){
            String userId =(String) messageSendDto.getExtendData();
            //删除redis中用户与群组的关系
            redisComponent.removeUserContact(userId,messageSendDto.getContactId());
            //关闭用户与群组间的通道
            Channel userChannel = USER_CONTEXT_MAP.get(userId);
            if(userChannel == null){
                return;
            }
            channelGroup.remove(userChannel);
        }
        //解散群组:使用close()方法
        if(MessageTypeEnum.DISSOLUTION_GROUP.equals(messageType)){
            GROUP_CONTEXT_MAP.remove(messageSendDto.getContactId());
            channelGroup.close();
        }
    }


    /*
     * 对群组的管理，维护群组间的映射
     * 键：组id 值：channel
     * ChannelGroup是Netty对多个channel进行管理
     */
    private void add2Group(String groupId, Channel channel) {
        //将某个channel加入指定groupId的channelGroup中
        /*
         理解一下channelGroup的生命周期：当创建出一个channelGroup与群id关联后存在GROUP_CONTEXT_MAP中，该channelGroup会持久化存在，动态变化的只有channelGroup中的channel，而GROUP_CONTEXT_MAP中只保存了该群组channelGroup的引用，
         */
        ChannelGroup channelGroup = GROUP_CONTEXT_MAP.get(groupId);//获取该群组channel
        if(channelGroup == null){//该群组channel不存在,则新建一个channelGroup管理它
            channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
            //这里隐藏了一句：GROUP_CONTEXT_MAP.put(groupId, channelGroup); // 隐式创建并关联，因为ConcurrentHashMap接口提供了原子性的putIfAbsent()方法，
            //当调用 GROUP_CONTEXT_MAP.get(groupId) 返回 null 时，新建的 ChannelGroup 需要存储到 map 中
        }
        if(channel ==  null){
            return;
        }
        channelGroup.add(channel);//将该用户的channel加入channelGroup
    }

    /**
     * 添加用户channel到组channel
     * @param userId
     * @param groupId
     */
    public void addUser2Group(String userId, String groupId) {
        //根据userID找到该用户对应的channel，再将其加入对应的群组
        Channel channel = USER_CONTEXT_MAP.get(userId);//获取用户通道
        add2Group(groupId, channel);

    }

//    /**
//     * 处理撤回消息
//     * @param messageSendDto
//     * @param contactType
//     */
//    private void handleWithdrawMessage(MessageSendDto withdrawDto, UserContactTypeEnum contactType) {
//        Long withdrawMessageId = messageSendDto.getWithdrawMessageId();
//
//        // 根据会话类型发送给所有相关用户
//        if (contactType == UserContactTypeEnum.USER) {
//             //单聊：发送给发送方和接收方
//            handleSingleChatWithdraw(withdrawDto);
//        } else {
//            // 群聊：发送给所有群成员
//            handleGroupChatWithdraw(withdrawDto);
//        }
//    }
//
//    /**
//     * 处理单聊撤回消息 -- 通过查询会话id获取会话中的成员
//     * @param withdrawDto
//     */
//    private void handleSingleChatWithdraw(MessageSendDto withdrawDto) {
//        String sessionId = withdrawDto.getSessionId();
//
//        // 查询该会话的所有参与者
//        ChatSessionUserQuery query = new ChatSessionUserQuery();
//        query.setSessionId(sessionId);
//        List<ChatSessionUser> sessionUsers = chatSessionUserMapper.selectList(query);
//
//        if (sessionUsers == null || sessionUsers.size() != 2) {
//            logger.error("单聊会话成员数量异常，sessionId: {}", sessionId);
//            return;
//        }
//        // 给每个参与者发送撤回通知
//        for (ChatSessionUser sessionUser : sessionUsers) {
//            MessageSendDto userNotification = new MessageSendDto();
//            BeanUtils.copyProperties(withdrawDto, userNotification);
//            userNotification.setContactId(sessionUser.getUserId());
//
//            // 可以根据需要调整提示内容
//            if (!sessionUser.getUserId().equals(withdrawDto.getSendUserId())) {
//                // 对接收方显示"对方撤回了一条消息"
//                userNotification.setMessageContent("对方撤回了一条消息");
//            } else {
//                // 对发送方显示"你已撤回消息"
//                userNotification.setMessageContent("你已撤回消息");
//            }
//
//            sendMsg2User(userNotification);
//        }
//
//    }

//    /**
//     * 处理群聊撤回
//     */
//    private void handleGroupChatWithdraw(MessageSendDto withdrawDto) {
//        String groupId = withdrawDto.getContactId();
//
//        // 直接使用群组广播
//        sendMsg2Group(withdrawDto);
//
//        // 如果是撤回提示消息，还需要更新会话的最后消息
//        if (withdrawDto.getIsWithdrawTip() != null && withdrawDto.getIsWithdrawTip()) {
//            updateGroupSessionLastMessage(withdrawDto);
//        }
//    }


//    /**
//     * 更新群会话的最后消息
//     */
//    private void updateGroupSessionLastMessage(MessageSendDto withdrawDto) {
//        ChatSession updateSession = new ChatSession();
//        updateSession.setLastMessage(withdrawDto.getMessageContent());
//        updateSession.setLastReceiveTime(withdrawDto.getSendTime());
//
//        chatSessionMapper.updateBySessionId(updateSession, withdrawDto.getSessionId());
//    }
}
